VSCode 远程开发链路的完整优化过程
Table of Contents
- 一、设备命名与网络拓扑说明
- 一、设备命名与网络拓扑说明
- 二、V1.0:只依赖 Tailscale 自己的连通性(DERP 模式)
- 三、V2.0:用宿舍 Windows 电脑做临时 SOCKS5 代理
- 3.1 在 Windows 上寻找可用的 SOCKS5 服务
- 3.2 在 C 上通过 SOCKS5 间接 SSH 到 A
- 四、V3.0:用 OpenWrt 路由器提供 24/7 的 Tailscale SOCKS5
- 4.1 安装 Tailscale:绕过坏掉的 opkg 源
- 4.2 利用 tailscaled 自带 SOCKS5 功能
- 4.3 用 rc.local 做一个”够用”的开机自启
- 五、V4.0:通过 SSH 配置把”复杂链路”封装到一个 Host 名里
- 5.1 最终版 ~/.ssh/config
- 5.2 连接复用带来的体验差异
- 六、常见问题与排查思路
- 6.1 仍然走 DERP,而不是走 B 的 SOCKS5
- 6.2 路由器上的 tailscaled 没有正确启动
- 6.3 代理端口被防火墙拦住
- 七、总结与反思:把复杂度留给网络,把接口留给自己
VSCode 远程开发链路的完整优化过程
这篇文章记录的是我为了让 VSCode Remote 在跨国环境下尽量接近本地体验,对网络链路做的一轮“事无巨细”的调整。
场景很简单:
- 一台工作站在中国,作为主要开发环境;
- 我在德国上学,需要在笔记本上长期远程开发;
- 远程开发主要依赖 VSCode Remote + SSH。
中间经历了几个阶段:
- 直接用 Tailscale,结果长期走 DERP 中继,延迟在 250ms 左右且不稳定;
- 临时用宿舍 Windows 电脑做 SOCKS5 代理,强制把流量走一条稳定的「C → 宿舍 → 中国」路径;
- 把代理迁移到 24/7 在线的 OpenWrt 路由器上,用 tailscaled 自带 SOCKS5;
- 最后通过
~/.ssh/config把 VSCode/SSH 的体验调到比较舒适的状态。
文章里会尽量交代清楚:每一步为什么做、做完之后具体有什么变化。
一、设备命名与网络拓扑说明
一、设备命名与网络拓扑说明
为了和前几篇文章保持一致,这里统一一下设备命名(后续文章也会沿用这一套):
-
设备 A:Linux 工作站(中国大陆)
- Linuxmint 系统,主要开发环境;
- 安装了 Tailscale;
- 对外只开放一个自定义 SSH 端口,例如
55905。
-
设备 B:OpenWrt 路由器(德国学生宿舍)
- 红米 AX6,刷了 AE86Wrt(定制 OpenWrt 系统);
- 24/7 在线;
- 已接入 Tailscale 网络;
- 作为后面”最终版”代理的承载设备。
-
设备 C:MacBook Air(德国)
- 便携开发设备,在宿舍和工位之间通勤使用;
- VSCode Remote + SSH 的客户端;
- 同时也是调试各种链路的主力设备。
-
设备 D:Windows 电脑(德国学生宿舍)
- 长期位于德国学生宿舍;
- 与设备 B 在同一个局域网;
- 中间阶段曾作为临时代理机使用。
Tailscale 网络中,各设备有各自的虚拟 IP,例如:
- 设备 A:
100.100.1.1 - 设备 B:
100.100.1.4 - 设备 D:
100.100.1.6
实际 IP 不重要,重点是:所有设备已经在同一个 Tailscale 网络里,可以互相 ping 通。
后文为了便于阅读,会使用自然称呼(如”工作站”、“路由器”、“MacBook”、“Windows电脑”)而不是生硬的代号。
二、V1.0:只依赖 Tailscale 自己的连通性(DERP 模式)
最开始,我的思路很朴素:既然所有设备都装了 Tailscale,那我直接从 MacBook用 Tailscale 去连工作站就好了。
在 MacBook 上测试:
# 在 MacBook 上执行
tailscale ping 100.100.1.1
输出类似:
pong from workstation (100.100.1.1) via DERP(hk1) in 248ms
pong from workstation (100.100.1.1) via DERP(hk1) in 247ms
...
direct connection not established
关键信息有两点:
via DERP(hk1):说明当前流量是通过 Tailscale 在香港的 DERP 中继节点转发的,并没有打通点对点;~250ms延迟:对于 SSH 只敲命令而言还勉强能用,但对于 VSCode Remote 这种大量小交互、频繁文件同步的场景,体验非常差。
实际体验:
- VSCode Remote 经常在连接/初始化阶段卡住;
- 操作稍微密集一点,就会出现卡顿、断连;
- 即使用了
ServerAliveInterval这种保活配置,也只是“不断线但很卡”。
这时我意识到,仅仅“能连上”不足以支撑日常开发:链路质量(延迟、抖动)同样关键。
三、V2.0:用宿舍 Windows 电脑做临时 SOCKS5 代理
在梳理网络拓扑时,我发现了一个有用的事实:
- MacBook ↔ Windows 电脑:Tailscale 能打通直连,延迟很低;
- Windows 电脑 ↔ 工作站:Tailscale 也能打通直连,延迟相对稳定。
换句话说,MacBook ↔ Windows 电脑 ↔ 工作站 这条路径,比直接 MacBook ↔ 工作站 的 DERP 中继要靠谱得多。
只要我把 SSH 流量”强制”走 MacBook → Windows 电脑 → 工作站 这条路径,就能绕开 DERP,走两段 P2P:
- MacBook → Windows 电脑:德国境内;
- Windows 电脑 → 工作站:德国宿舍 → 中国(跨国,但相对稳定)。
这就需要在 Windows 电脑上提供一个 SOCKS5 代理端口,然后在 MacBook 的 SSH 配置里使用 ProxyCommand。
3.1 在 Windows 上寻找可用的 SOCKS5 服务
设备 D 是 Windows 电脑,已经安装了几种代理相关工具,但实际可用的不多。
几个尝试过程简单列一下:
-
Tailscale 内置 SOCKS5(失败)
Tailscale 在 Linux/macOS 上支持
tailscale set --sockshost,可以直接开启一个 SOCKS5 代理。但在 Windows PowerShell 下,这个命令不可用,运行后只会提示参数不支持。 结论:Windows 版本 Tailscale 暂时不支持这个功能。 -
Clash Verge(不适合当前场景)
宿舍电脑上安装了 Clash Verge,理论上也能对外提供代理。但它是“订阅优先”的设计,界面偏向于“作为客户端用远端节点上网”,而不是“在本地开一个通用 SOCKS5 给别人用”。 实际尝试时,它并没有按预期在本机监听一个稳定端口,对我这个单纯想要“开一个裸 SOCKS5 转发 Tailscale 流量”的场景不太友好。
-
v2rayN(最终选择)
最后我换用了 v2rayN,对它的期待非常简单:在本地开启一个 SOCKS5 端口,把所有流量转发到 Tailscale 虚拟网卡上。 配置思路是:
- 创建一个最小化的
socks5出站; - 入站则直接监听本地端口(比如 10800);
- 不使用任何订阅,只用本地转发。
启动后,注意了一点:系统里可能已经有其它
xray.exe进程占住了默认端口(1080),需要通过任务管理器或netstat检查,清理冲突进程,最后确定一个干净的端口,例如10800。 - 创建一个最小化的
3.2 在 C 上通过 SOCKS5 间接 SSH 到 A
假设:
- 设备 D 在 Tailscale 上的 IP:
100.100.1.6 - v2rayN 在 D 上监听的端口:
10800
在 C 上可以用 nc 先测一下代理端口是否可达:
nc -vz 100.100.1.6 10800
# Connection to 100.100.1.6 port 10800 succeeded!
然后用 ProxyCommand 做一次 SSH 尝试:
ssh -p 55905 \
-o ProxyCommand="nc -X 5 -x 100.100.1.6:10800 %h %p" \
ruichen@100.100.1.1
这里:
100.100.1.1是设备 A 的 Tailscale IP;55905是 A 上 SSH 的监听端口;nc -X 5 -x 100.100.1.6:10800 %h %p表示:通过 SOCKS5 代理100.100.1.6:10800去访问目标%h:%p。
V2.0 的效果:
- 延迟明显比直接走 DERP 要舒服得多,VSCode Remote 的卡顿情况有所缓解;
- 但有一个致命缺点:必须保证 D 这台 Windows 电脑始终开机,并且 v2rayN 在后台运行。这在宿舍日常环境里并不现实。
因此,这一版只能算临时解决方案。
四、V3.0:用 OpenWrt 路由器提供 24/7 的 Tailscale SOCKS5
真正”常驻在线”的是 OpenWrt 路由器——宿舍的红米 AX6。它一直通电、功耗低、不用担心有人把它关机。
于是,把代理服务从 Windows 电脑挪到路由器上,就很自然了:
MacBook 始终通过
路由器的 SOCKS5去访问工作站。
4.1 安装 Tailscale:绕过坏掉的 opkg 源
路由器使用的是基于 OpenWrt 的 AE86Wrt 固件。理想状态下,我只需要:
opkg update
opkg install tailscale tailscaled
但现实是:
opkg update报了大量 404 / 签名错误,源已经不再维护;- 内置的应用商店(如 iStore)也无法使用;
- 从 OpenWrt 官方去下载对应架构的
.ipk,也频繁遇到 404。
在确认这是“固件本身的源配置已经老旧”的问题后,我选择了另一条路:直接用 Tailscale 官方提供的静态编译二进制。
大致步骤:
# 1. 下载 tailscale 的 arm64/arm 静态包(根据路由器 CPU 架构选择)
cd /tmp
wget https://pkgs.tailscale.com/stable/tailscale_..._mipsle.tgz
tar xzf tailscale_..._mipsle.tgz
# 2. 把二进制挪到 PATH 下
mv tailscale tailscaled /usr/bin/
chmod +x /usr/bin/tailscale /usr/bin/tailscaled
然后在路由器上登录 Tailscale,加入已有网络(可以用 tailscale up 或事先生成 auth key)。
4.2 利用 tailscaled 自带 SOCKS5 功能
很多教程会建议在路由器上额外安装 microsocks 之类的软件,但 tailscaled 本身就支持开启 SOCKS5:
tailscaled \
--state=/var/lib/tailscale/tailscaled.state \
--socks5-server=0.0.0.0:1080
这行命令会:
- 启动 tailscaled 并读取/保存状态到指定路径;
- 在所有网口上监听一个 SOCKS5 端口
1080。
只要 B 加入了 Tailscale 网络,C 就能访问 B 的 100.100.1.4:1080(前提是防火墙允许)。
4.3 用 rc.local 做一个”够用”的开机自启
在理想世界里,我们应该为 tailscaled 写一个完整的 /etc/init.d/tailscaled 脚本。但在这台定制固件路由器上,init 脚本调试成本不低。最后我选了一个更简单的方案:直接用 /etc/rc.local。
示意:
# /etc/rc.local 中,exit 0 之前加入一行
nohup /usr/bin/tailscaled \
--state=/var/lib/tailscale/tailscaled.state \
--socks5-server=0.0.0.0:1080 \
>/var/log/tailscaled.log 2>&1 &
exit 0
这样每次路由器重启时:
- tailscaled 会自动拉起;
- SOCKS5 端口
1080会自动监听; - 日志写到
/var/log/tailscaled.log,方便排查。
最终,路由器成为一个24/7 在线的 Tailscale SOCKS5 代理,并在 Tailscale 管理面板中将路由器的 IP 固定为 100.100.1.4,方便在 SSH 配置里引用。
五、V4.0:通过 SSH 配置把”复杂链路”封装到一个 Host 名里
到这一步,底层链路已经稳定了:
MacBook →(Tailscale)→ 路由器(OpenWrt)→(Tailscale)→ 工作站
接下来要解决的是:把复杂度藏起来,让 VSCode/SSH 只感知到一个稳定的”workstation”。
需求有三个:
- 我不想每次都手写
ProxyCommand=...; - 无论在办公室还是宿舍,VSCode 和 Copilot 看见的都是同一个
Host workstation,以便共享历史、缓存和上下文; - 尽量利用 SSH 的连接复用和保活机制,减少 VSCode 频繁重连的开销。
5.1 最终版 ~/.ssh/config
在 MacBook 上,我最后整理出这样一份配置:
# ================================================================
# 工作站(中国)- Linux 主机
# ================================================================
Host workstation
HostName 100.100.1.1 # 工作站的 Tailscale IP
User ruichen
Port 55905 # 工作站上的 SSH 自定义端口
# 核心:所有连接都通过路由器(OpenWrt)的 SOCKS5
ProxyCommand nc -X 5 -x 100.100.1.4:1080 %h %p
### 5.1 最终版 `~/.ssh/config`
在 C 上,我最后整理出这样一份配置:
```sshconfig
# ================================================================
# A(中国)- 4090 工作站
# ================================================================
Host workstation
HostName 100.100.1.1 # 设备 A 的 Tailscale IP
User ruichen
Port 55905 # A 上的 SSH 自定义端口
# 核心:所有连接都通过 B(OpenWrt 路由器)的 SOCKS5
ProxyCommand nc -X 5 -x 100.100.1.4:1080 %h %p
# ================================================================
# 全局设置,作用于所有 Host(包括 workstation)
# ================================================================
Host *
# 身份认证
IdentityFile ~/.ssh/id_ed25519
# --- 1. 保活设置:尽量“不断线” ---
ServerAliveInterval 30 # 每 30 秒发一个心跳包
ServerAliveCountMax 999999 # 理论上允许非常多次失败
TCPKeepAlive yes
# --- 2. 性能:连接复用 ---
Compression yes # 适度压缩,小带宽环境下有帮助
# 启用 SSH 的连接复用(ControlMaster 模式)
ControlMaster auto
ControlPersist yes # master 连接在后台长期保持
ControlPath ~/.ssh/cm-%r@%h:%p
这份配置的效果是:
- 日常使用时只需要敲
ssh workstation; - VSCode Remote 里也只声明一个 Host:
workstation; - 所有连接都统一走
C → B:1080 → A:55905,不会因为在宿舍/办公室而改变。
5.2 连接复用带来的体验差异
启用 ControlMaster / ControlPersist 后,SSH 的行为会发生一个很“微妙但重要”的变化:
-
第一次连接时,SSH 会建立一个“master”连接,并显示完整的 MOTD 等信息:
ssh workstation # 显示欢迎信息、GPU 状态、登录提示等 -
再次连接时,只要 master 还在,新的连接会直接复用已有通道,不再重新认证,也不会重新打印 MOTD:
ssh workstation # 几乎瞬间进入 shell,只有一行类似: # Last login: Fri Nov 14 03:29:00 2025 from 100.100.1.6
对于 VSCode 来说,这种“第二次及之后”的连接行为非常友好:
- 建立连接更快;
- 多个终端/扩展可以共享同一条底层 SSH 通道;
- Copilot / 终端历史等,也都建立在同一个
workstation身份之上。
六、常见问题与排查思路
这里单独列一些过程中的坑,便于以后查阅。
6.1 仍然走 DERP,而不是走 B 的 SOCKS5
现象:
tailscale ping显示仍然via DERP(hk1);- 但
ssh workstation看起来“勉强能用”。
可能原因:
- SSH 没走 SOCKS5,而是直接从 C → A 的 Tailscale 连接;
~/.ssh/config中的ProxyCommand被其它配置覆盖、或书写错误。
排查:
- 临时加上
-vvv参数,看 SSH 的调试输出,确认是否执行了ProxyCommand; - 用
ss -tnp查看 C 本机是否建立了到100.100.1.4:1080的连接。
6.2 路由器上的 tailscaled 没有正确启动
现象:
- C 无法连接
100.100.1.4:1080; - Tailscale 控制台里看不到 B 在线。
排查:
- 在 B 上查看日志文件(例如
/var/log/tailscaled.log); - 使用
ps | grep tailscaled确认进程是否存活; - 手动执行一次启动命令,排除参数问题后再写入
rc.local。
6.3 代理端口被防火墙拦住
在部分路由器固件中,LAN→Tailscale 或本机端口的访问会受到防火墙规则限制。
排查:
- 使用
netstat -lnp | grep 1080确认 tailscaled 在监听; - 确认防火墙中允许来自 LAN/Tailscale 网段访问本机 1080 端口;
- 临时放开相关规则测试,如果可行再收紧到合理范围。
七、总结与反思:把复杂度留给网络,把接口留给自己
这次从 V1.0 到 V4.0 的调整,回头看下来,技术难度其实不算高,更像是一系列“把事情想清楚”的过程:
-
先承认物理现实:跨国延迟无法消灭
250ms 的 RTT 是客观存在的,即便链路优化到极致,也不会变成 10ms。 真正能优化的是:减少 DERP 中继、减少额外跳数、减少重复握手。
-
识别出网络中的“稳定节点”,让它承担关键角色
在我的场景里,宿舍的 OpenWrt 路由器(B)就是那个几乎永远在线的节点。 把 SOCKS5 放在 B 上,而不是时开时关的 Windows 电脑(D),能显著降低“链路突然消失”的概率。
-
尽量用现有工具的原生能力,而不是叠加新组件
tailscaled 自带 SOCKS5,比在上面再套一层 microsocks/v2ray 更简单、可控。 同理,SSH 自带的
ProxyCommand和ControlMaster,足以处理大部分 VSCode 场景。
这篇文章更多是对这条链路的“验收报告”。后续如果再增加新的节点(比如家里的 NAS、实验用 GPU 服务器),我会沿用同样的思路:先把网络拓扑画清楚,再决定每台设备承担什么职责,而不是一上来就堆命令。